#include <Arduino.h>
#include <WString.h>
#include <CallBack.h>
#include <avr/eeprom.h>
#include <avr/pgmspace.h>
// todo; add built in command to set command echo on/off

struct settings_t
{
  int  eepromMagicNumber;
  char eepromId[buflen];
  char eepromGid[buflen];
  int  eepromDelay;
} settings;

// utility functions to conserve ram, i. e. enable storing and printing from pgm mem instead.
void pgmPrint (PGM_P s) {
	char c;
	while ((c = pgm_read_byte(s++)) != 0)
	Serial.print(c);
}

void pgmPrintLn (PGM_P s) {
	char c;
	while ((c = pgm_read_byte(s++)) != 0)
	Serial.print(c);
	Serial.println("");
}

void CallBack::writeSettings(int mn, String sId, String sGid, int dl) {

	settings.eepromMagicNumber = mn;
	sId.toCharArray(settings.eepromId, buflen);
	sGid.toCharArray(settings.eepromGid, buflen);
	settings.eepromDelay = dl;
	
	eeprom_write_block((const void*)&settings, (void*)0, sizeof(settings));
	readSettings();
}

void CallBack::readSettings() {
	
	eeprom_read_block((void*)&settings, (void*)0, sizeof(settings));
	
	myId = settings.eepromId;
	myGid = settings.eepromGid;
	myDelay = settings.eepromDelay;
}

int CallBack::readMagic() {
	
	eeprom_read_block((void*)&settings, (void*)0, sizeof(settings));
	return (settings.eepromMagicNumber);
}

int CallBack::stoi(String s) { // convenience function to convert args to int
	char ca[s.length()+1];
	s.toCharArray(ca, s.length()+1);
	return atoi(ca);
}

float CallBack::stof(String s) { // convenience function to convert args to float
	char ca[s.length()+1];
	s.toCharArray(ca, s.length()+1);
	return atof(ca);
}

CallBack::CallBack(CallBackDef funcs[], int n, String id, String gid, String progDescr, int delay) {
	myFuncs = &funcs[0]; // function callback struct array
	noof = n; // number of functions
	myId = id; // identifier for specific device
	myGid = gid; // grop identifyer for group of devices
	myProgDescr = progDescr; // description (used in help function)
	myDelay = delay; // response delay (ms) for group requests (to place responses in different time slots)
	cmdBuf = "";
	
	//int tmpMagic = 0;
	
	if(readMagic() == refMagicNumber) { // settings have been saved
		readSettings();
	} else {
		writeSettings(refMagicNumber, myId, myGid, myDelay);
	}
}

void CallBack::cmdCheck() {
	char c=0;
	if((millis() - lastSerialMillis) > 50) {
		cmdBuf = "";
	}
	if(Serial.available() > 0) {
		lastSerialMillis = millis();
		// check serial buffer for commands
		while (Serial.available() > 0) {
			// read the incoming bytes
			c = Serial.read();
			cmdBuf += c;
		}
		if(c == ';') {
			pgmPrint(PSTR(">"));
			Serial.println(cmdBuf);
			Serial.flush();
			parseCmd(cmdBuf);
			cmdBuf = "";
		}
	}
}

void CallBack::parseCmd(String cb) {
	bool cmdForMe = false;
	String addrField = "", addrType = "", addr = "";
	tmpDelay = 0;
	
	cb.toLowerCase();
	cb.replace(";", ":");
	addrField = cb.substring(0,cb.indexOf(':'));
	if(addrField == "") {// empty first field? this is for me
		cmdForMe = true;
		cb = cb.substring(1); // remove part from buffer
	} 
	else {
		addrType = cb.substring(0,cb.indexOf('='));
		cb = cb.substring(cb.indexOf('=') + 1); // remove part from buffer
		addr = cb.substring(0, cb.indexOf(':'));
		cb = cb.substring(cb.indexOf(':') + 1); // remove part from buffer
		if(addrType == "id" && addr == myId) {cmdForMe = true;}
		if(addrType == "gid" && addr == myGid) {
			cmdForMe = true;
			tmpDelay = myDelay; // use delayed response only if group address
		}
	}
	if(cmdForMe) { // address validation ok, let's check commands
		String kw = cb.substring(0, cb.indexOf(':'));
		cb = cb.substring(cb.indexOf(':') + 1);
		kw.toLowerCase();

		int argIndex = 0;
		while(cb.length() > 0) {
			String param = cb.substring(0, cb.indexOf(':'));
			cb = cb.substring(cb.indexOf(':') + 1);
			myArgv[argIndex++] = param;
		}

		//checking built in commands first
		if(kw == "help") 			{ if (argIndex == 0) { help(); } else { nok("incorrect number of arguments"); }}
		else if (kw == "ping") 		{ if (argIndex == 0) { ok(); } else { nok("incorrect number of arguments"); }}
		else if (kw == "setid") 	{ if (argIndex == 1) { setid(myArgv[0]); } else { nok("incorrect number of arguments"); }}
		else if (kw == "setgid") 	{ if (argIndex == 1) { setgid(myArgv[0]); } else { nok("incorrect number of arguments"); }}
		else if (kw == "setdelay") 	{ if (argIndex == 1) { setdelay(myArgv[0]); } else { nok("incorrect number of arguments"); }}
		else if (kw == "settings") 	{ if (argIndex == 0) { printSettings(); } else { nok("incorrect number of arguments"); }}
		else {
			// loop through list of user funcs to match keywords
			for(int i; i < noof; i++) {
				String storedKw = myFuncs[i].keyword;
				storedKw.toLowerCase();
				if(storedKw == kw) {
					run(i, argIndex, myArgv);
				}
			}
		}
	}
}

void CallBack::setid(String s) {
	writeSettings(refMagicNumber, s, myGid, myDelay);
	ok();
}

void CallBack::setgid(String s) {
	writeSettings(refMagicNumber, myId, s, myDelay);
	ok();
}

void CallBack::setdelay(String s) {
	writeSettings(refMagicNumber, myId, myGid, stoi(s));
	ok();
}

void CallBack::printSettings() {
	//Serial.print("id:    "); Serial.println(myId);
	pgmPrint(PSTR("id:    ")); Serial.println(myId);
	pgmPrint(PSTR("gid:   ")); Serial.println(myGid);
	pgmPrint(PSTR("delay: ")); Serial.println(myDelay);
}

void CallBack::run(int funcIndex, int argc, String argv[PRM_LEN]) {
	if(argc == myFuncs[funcIndex].args) {
		myFuncs[funcIndex].fp(argv);
	} else {
		nok("incorrect number of arguments");
	}
}

void CallBack::respond(String s) {
	delay(tmpDelay);
	pgmPrint(PSTR("<id="));
	Serial.print(myId);
	pgmPrint(PSTR(":"));
	Serial.print(s);
	pgmPrintLn(PSTR(";"));
	tmpDelay = 0;
}

void CallBack::ok() {
	respond("ok");
}

void CallBack::nok(String s) {
	String r = "nok:";
	r.concat(s);
	respond(r);
}

void CallBack::help() {
	Serial.println(myProgDescr);
	Serial.println();
	pgmPrint(PSTR("Delayed group response: "));
	Serial.print(myDelay);
	pgmPrintLn(PSTR("ms"));
	Serial.println("");
	pgmPrint(PSTR("Replace <addr> with id="));
	Serial.print(myId);
	pgmPrintLn(PSTR(" for this device"));
	pgmPrint(PSTR("Replace <addr> with gid="));
	Serial.print(myGid);
	pgmPrintLn(PSTR(" for this device group"));
	pgmPrintLn(PSTR("Replace <addr> with empty string for local access"));
	Serial.println("");
	pgmPrintLn(PSTR("Device Commands: "));
	pgmPrintLn(PSTR("<addr>:help;"));
	pgmPrintLn(PSTR("<addr>:ping;"));
	pgmPrintLn(PSTR("<addr>:setid:<id>;"));
	pgmPrintLn(PSTR("<addr>:setgid:<gid>;"));
	pgmPrintLn(PSTR("<addr>:setdelay:<delay>;"));
	pgmPrintLn(PSTR("<addr>:settings;"));
	Serial.println("");
	pgmPrintLn(PSTR("Application Commands: "));
	for(int i = 0; i < noof; i++) {
		pgmPrint(PSTR("<addr>:"));
		Serial.print(myFuncs[i].keyword);
		Serial.print(myFuncs[i].argDesc);
		pgmPrintLn(PSTR(";"));
	}
}